package com.flow.framework.web.config;

import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.flow.framework.base.properties.FrameworkBaseConfigProperties;
import com.flow.framework.base.service.access.log.IAccessLogService;
import com.flow.framework.base.service.system.ops.ISystemOpsSecurityService;
import com.flow.framework.common.datetime.CommonDateTimeFormatter;
import com.flow.framework.common.json.JsonObject;
import com.flow.framework.common.util.verify.VerifyUtil;
import com.flow.framework.core.service.properties.ISystemConfigPropertiesService;
import com.flow.framework.web.advice.WebExceptionAdvice;
import com.flow.framework.web.advice.WebResponseBodyAdvice;
import com.flow.framework.web.controller.ErrorController;
import com.flow.framework.web.filter.RequestResponseBodyFilter;
import com.flow.framework.web.filter.SystemOpsSecurityFilter;
import com.flow.framework.web.helper.AccessLogHelper;
import com.flow.framework.web.service.web.IResponseBodyAdviceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpProperties;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;

import java.beans.PropertyEditorSupport;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * 框架web配置
 *
 * @author luoguopiao
 * @version 0.0.1
 * @date 2022/12/24
 */
@Configuration
@Slf4j
public class FrameworkWebConfig {

    @Bean
    @Primary
    @ConditionalOnMissingBean
    Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return builder -> builder
                .serializerByType(LocalDateTime.class,
                        new LocalDateTimeSerializer(CommonDateTimeFormatter.COMMON_DATETIME_FORMATTER))
                .serializerByType(LocalDate.class,
                        new LocalDateSerializer(CommonDateTimeFormatter.COMMON_DATE_FORMATTER))
                .deserializerByType(LocalDateTime.class,
                        new LocalDateTimeDeserializer(CommonDateTimeFormatter.COMMON_DATETIME_FORMATTER))
                .deserializerByType(LocalDate.class,
                        new LocalDateDeserializer(CommonDateTimeFormatter.COMMON_DATE_FORMATTER));
    }

    @Bean
    @ConditionalOnMissingBean
    AccessLogHelper accessLogHelper(IAccessLogService accessLogService) {
        return new AccessLogHelper(accessLogService);
    }

    /**
     * HttpMessageConverter中，没有对直接使用字符串接收body的消息转换器，故这里需要注册一个自定义实现
     *
     * @param httpProperties http配置
     * @return 直接使用字符串接收body的消息转换器
     */
    @Bean
    @Primary
    StringHttpMessageConverter customizationStringHttpMessageConverter(HttpProperties httpProperties) {
        StringHttpMessageConverter converter = new StringHttpMessageConverter(
                httpProperties.getEncoding().getCharset());
        converter.setWriteAcceptCharset(false);
        return converter;
    }


    @Bean
    @ConditionalOnMissingBean
    ErrorController errorController(ErrorAttributes errorAttributes, List<ErrorViewResolver> errorViewResolvers,
                                    ISystemConfigPropertiesService systemConfigPropertiesService) {
        return new ErrorController(errorAttributes, errorViewResolvers, systemConfigPropertiesService);
    }

    @Bean
    @ConditionalOnMissingBean
    WebExceptionAdvice webExceptionAdvice(ErrorController errorController) {
        return new WebExceptionAdvice(errorController);
    }

    @Bean
    @ConditionalOnMissingBean
    WebResponseBodyAdvice webResponseBodyAdvice(@Autowired(required = false) List<IResponseBodyAdviceService> responseBodyAdviceServices,
                                                ISystemConfigPropertiesService systemConfigPropertiesService,
                                                FrameworkBaseConfigProperties frameworkBaseConfigProperties,
                                                AccessLogHelper accessLogHelper) {
        return new WebResponseBodyAdvice(responseBodyAdviceServices, systemConfigPropertiesService, frameworkBaseConfigProperties,
                accessLogHelper);
    }

    @Bean
    @ConditionalOnMissingBean
    RequestResponseBodyFilter requestResponseBodyFilter(IAccessLogService accessLogService, AccessLogHelper accessLogHelper,
                                                        FrameworkBaseConfigProperties frameworkBaseConfigProperties) {
        return new RequestResponseBodyFilter(accessLogService, accessLogHelper, frameworkBaseConfigProperties);
    }

    @Bean
    @ConditionalOnMissingBean
    SystemOpsSecurityFilter systemOpsSecurityFilter(ISystemOpsSecurityService systemOpsSecurityService) {
        return new SystemOpsSecurityFilter(systemOpsSecurityService);
    }


    /**
     * 全局入参处理，格式化LocalDate和LocalDateTime
     *
     * @param webDataBinder 在URL中传递的时间格式转化
     */
    @InitBinder
    public void initBinder(WebDataBinder webDataBinder) {
        webDataBinder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                if (!VerifyUtil.isEmpty(text)) {
                    setValue(LocalDate.parse(text, CommonDateTimeFormatter.COMMON_DATE_FORMATTER));
                }
            }
        });
        webDataBinder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                if (!VerifyUtil.isEmpty(text)) {
                    setValue(LocalDateTime.parse(text, CommonDateTimeFormatter.COMMON_DATETIME_FORMATTER));
                }
            }
        });
    }


    public static class StringHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

        /**
         * The default charset used by the converter.
         */
        public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;


        @Nullable
        private volatile List<Charset> availableCharsets;

        private boolean writeAcceptCharset = false;


        /**
         * A default constructor that uses {@code "ISO-8859-1"} as the default charset.
         *
         * @see #StringHttpMessageConverter(Charset)
         */
        public StringHttpMessageConverter() {
            this(DEFAULT_CHARSET);
        }

        /**
         * A constructor accepting a default charset to use if the requested content
         * type does not specify one.
         */
        public StringHttpMessageConverter(Charset defaultCharset) {
            super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
        }


        /**
         * Whether the {@code Accept-Charset} header should be written to any outgoing
         * request sourced from the value of {@link Charset#availableCharsets()}.
         * The behavior is suppressed if the header has already been set.
         * <p>As of 5.2, by default is set to {@code false}.
         */
        public void setWriteAcceptCharset(boolean writeAcceptCharset) {
            this.writeAcceptCharset = writeAcceptCharset;
        }


        @Override
        public boolean supports(Class<?> clazz) {
            return String.class == clazz;
        }

        @Override
        protected String readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException {
            Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
            return StreamUtils.copyToString(inputMessage.getBody(), charset);
        }

        @Override
        protected Long getContentLength(Object str, @Nullable MediaType contentType) {
            String body = getBody(str);
            Charset charset = getContentTypeCharset(contentType);
            return (long) body.getBytes(charset).length;
        }


        @Override
        protected void addDefaultHeaders(HttpHeaders headers, Object s, @Nullable MediaType type) throws IOException {
            String body = getBody(s);
            if (headers.getContentType() == null) {
                if (type != null && type.isConcrete() && type.isCompatibleWith(MediaType.APPLICATION_JSON)) {
                    // Prevent charset parameter for JSON..
                    headers.setContentType(type);
                }
            }
            super.addDefaultHeaders(headers, body, type);
        }

        @Override
        protected void writeInternal(Object str, HttpOutputMessage outputMessage) throws IOException {
            String body = getBody(str);
            HttpHeaders headers = outputMessage.getHeaders();
            if (this.writeAcceptCharset && headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
                headers.setAcceptCharset(getAcceptedCharsets());
            }
            Charset charset = getContentTypeCharset(headers.getContentType());
            StreamUtils.copy(body, charset, outputMessage.getBody());
        }


        /**
         * Return the list of supported {@link Charset Charsets}.
         * <p>By default, returns {@link Charset#availableCharsets()}.
         * Can be overridden in subclasses.
         *
         * @return the list of accepted charsets
         */
        protected List<Charset> getAcceptedCharsets() {
            List<Charset> charsets = this.availableCharsets;
            if (charsets == null) {
                charsets = new ArrayList<>(Charset.availableCharsets().values());
                this.availableCharsets = charsets;
            }
            return charsets;
        }

        private Charset getContentTypeCharset(@Nullable MediaType contentType) {
            if (contentType != null && contentType.getCharset() != null) {
                return contentType.getCharset();
            } else if (contentType != null && contentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
                // Matching to AbstractJackson2HttpMessageConverter#DEFAULT_CHARSET
                return StandardCharsets.UTF_8;
            } else {
                Charset charset = getDefaultCharset();
                Assert.state(charset != null, "No default charset");
                return charset;
            }
        }

        private String getBody(Object str) {
            String body;
            if (!(str instanceof String)) {
                body = JsonObject.toString(str);
            } else {
                body = (String) str;
            }
            return body;
        }

    }
}
